认识设备树(四)

您所在的位置:网站首页 单板 flat 认识设备树(四)

认识设备树(四)

2024-01-19 17:58| 来源: 网络整理| 查看: 265

目录 前言1 从u-boot传参到__atags_pointer2 内核对设备树中平台信息的处理2.1 machine_desc2.2 源码分析2.2.1 setup_arch2.2.2 setup_machine_fdt2.2.3 of_flat_dt_match_machine 3 内核对设备树中运行时配置信息的处理3.1 of_scan_flat_dt3.2 解析/chosen节点3.3 解析根节点的{size,address}-cells属性3.4 解析/memory节点3.5 小结 4 内核对设备树中设备信息的处理4.1 内核对DTB所在内存的处理4.2 struct device_node和struct property4.3 从DTB到struct device_node示例4.4 unflatten_device_tree分析4.5 将device_node转换成platform_device4.5.1 哪些节点需要转换成platform_device4.5.2 在哪里做的转换工作4.5.3 浅析转换的过程4.5.4 如何处理平台设备对应的设备节点的子节点 4.6 小结 5 内核中设备树相关头文件的总结5.1 声明处理DTB文件的函数的头文件5.2 处理device_node的函数的头文件5.3 声明处理platform_device的函数的头文件 6 设备树在文件系统中的表示6.1 /sys与设备树6.2 /proc与设备树 参考文献

前言

本文关注的主要是内核如何处理DTB文件中记录的设备信息,会分析内核解析DTB文件的主体流程,不会关注所有细节,同时,也不会关注对特定设备信息的处理,比如对有关中断的设备信息的处理,相关内容在做相应模块的笔记时再细究(学习linux的中断管理时会分析内核对DTB中有关中断的部分的处理)。

1 从u-boot传参到__atags_pointer

前文已经说过,DTB文件由u-boot传递给内核,u-boot在跳转到内核时,会把一些关键的信息通过参数(实际使用通用寄存器r0、r1、r2)传递给内核:

r0:通常设置为0;r1:传递的是ATAGS时,通常设置为板子的machine id;传递的是设备树时,该参数无用;r2:通常设置为ATAGS或DTB在内存中的起始地址。

可见,当u-boot向内核传递DTB文件时,内核真正要关注的只有r2寄存器中存放的DTB文件的内存起始地址。内核会在启动的汇编阶段把这个地址保存到全局变量__atags_pointer,大体的过程如下:

/* 1、设置r13,跳转执行__enable_mmu */ ENTRY(stext) ...... ldr r13, =__mmap_switched ...... /* 跳转执行__enable_mmu */ b __enable_mmu ENDPROC(stext) /* 2、使能MMU,跳转执行__mmap_switched */ __enable_mmu: /* 在r0设置好打算写往MMU控制寄存器的值 */ ...... /* 跳转执行 */ b __turn_mmu_on ENTRY(__turn_mmu_on) mov r0, r0 instr_sync mcr p15, 0, r0, c1, c0, 0 @ write control reg mrc p15, 0, r3, c0, c0, 0 @ read id reg instr_sync mov r3, r3 /* r13 = __mmap_switched */ mov r3, r13 ret r3 __turn_mmu_on_end: ENDPROC(__turn_mmu_on) /* 3、在__mmap_switched中,将r2(保存DTB的地址)的值存到__atags_pointer,然后跳转start_kernel */ __mmap_switched: mov r7, r1 mov r8, r2 /* 把DTB的地址转存到r8 */ mov r10, r0 adr r4, __mmap_switched_data mov fp, #0 ARM( ldmia r4!, {r0, r1, sp} ) sub r2, r1, r0 mov r1, #0 bl memset @ clear .bss ldmia r4, {r0, r1, r2, r3} str r9, [r0] @ Save processor ID str r7, [r1] @ Save machine type /* 把DTB的地址写到全局变量__atags_pointer */ str r8, [r2] @ Save atags pointer cmp r3, #0 strne r10, [r3] @ Save control register values mov lr, #0 b start_kernel ENDPROC(__mmap_switched) __mmap_switched_data: .long __bss_start @ r0 .long __bss_stop @ r1 .long init_thread_union + THREAD_START_SP @ sp .long processor_id @ r0 .long __machine_arch_type @ r1 .long __atags_pointer @ r2 ......

kernel启动的汇编阶段结束后,会跳转执行start_kernel,此时__atags_pointer指向内存中的DTB文件。kernel对DTB的解析在start_kernel==>setup_arch函数中进行。kernel将DTB中的信息分为三类:

平台识别信息,通常指的是根节点的compatible、model属性记录的信息;运行时配置信息,通常指的是/chosen节点和/memory节点记录的信息;设备信息,指各个设备节点。

下文将分别介绍内核对这三种信息的处理。

2 内核对设备树中平台信息的处理 2.1 machine_desc

一个kernel镜像通常会支持很多板子,针对每种板子,kernel都会为其定义一个struct machine_desc的结构,其中就记录各个板子的硬件信息,比如板子的ID号、名字、支持的中断数量、初始化函数等。这样,在kernel启动时,可以根据u-boot传递的参数/DTB文件选则合适的machine_desc,从而正确的初始化当前硬件。

kernel会将一系列machine_desc集中存放在.init.arch.info节中,形成如同数组一样的内存分布:

.init.arch.info : { __arch_info_begin = .; *(.arch.info.init) __arch_info_end = .; }

并以符号__arch_info_begin和__arch_info_end记录该节的起始和结尾,如此一来,就可以向访问数组元素那样访问每个machine_desc。

在选则machine_desc时,kernel首先会获取DTB的根节点的compatible属性,将其中的一个或多个字符串与machine_desc的dt_compat成员记录的一个或多个字符串进行比较,当匹配时,返回相应的machine_desc。值得一提的是,compatible属性值中,位置靠前的字符串会优先比较,换句话说,位置越靠前说明该字符串指示的machine_desc越适合当前单板。

2.2 源码分析

有了2.1节的铺垫,接下来就可以进行源码分析了,从setup_arch开始:

2.2.1 setup_arch void __init setup_arch(char **cmdline_p) { const struct machine_desc *mdesc; /* 初始化一些处理器相关的全局变量 */ setup_processor(); /* 优先按照设备树获取machine_desc */ mdesc = setup_machine_fdt(__atags_pointer); /* 如果u-boot传递的不是DTB,则按照ATAGS获取machine_desc */ if (!mdesc) mdesc = setup_machine_tags(__atags_pointer, __machine_arch_type); ...... /* 记录获取到的machine_desc及其名字 */ machine_desc = mdesc; machine_name = mdesc->name; dump_stack_set_arch_desc("%s", mdesc->name); ...... /* 将boot_command_line的内容拷贝到cmd_line */ strlcpy(cmd_line, boot_command_line, COMMAND_LINE_SIZE); /* 输出指向启动参数的指针 */ *cmdline_p = cmd_line; ...... /* 根据DTB创建device_node树 */ unflatten_device_tree(); ...... #ifdef CONFIG_GENERIC_IRQ_MULTI_HANDLER /* 设置handle_arch_irq */ handle_arch_irq = mdesc->handle_irq; #endif ...... /* 调用machine_desc中注册的初始化函数 */ if (mdesc->init_early) mdesc->init_early(); } 2.2.2 setup_machine_fdt

由上文可知,setup_arch函数调用setup_machine_fdt解析设备树中的相关信息,并返回合适的machine_desc:

const struct machine_desc * __init setup_machine_fdt(unsigned int dt_phys) { const struct machine_desc *mdesc, *mdesc_best = NULL; /* 验证DTB文件是否存在:地址不为NULL && 文件头部magic正确 */ /* initial_boot_params = dt_phys */ if (!dt_phys || !early_init_dt_verify(phys_to_virt(dt_phys))) return NULL; /* 获取compatible属性并匹配合适的machine_desc */ mdesc = of_flat_dt_match_machine(mdesc_best, arch_get_next_mach); if (!mdesc) { /* 打印一些信息 */ ...... /* 把当前kernel支持的单板的名字和单板ID打印出来 */ /* 该函数不会返回(内部有死循环) */ dump_machine_table(); } /* 当DTB文件提供的数据有问题,这里会做一些修补工作 */ if (mdesc->dt_fixup) mdesc->dt_fixup(); /* 获取运行时配置信息,再第3节中细说 */ early_init_dt_scan_nodes(); /* 记录machine ID */ __machine_arch_type = mdesc->nr; return mdesc; } 2.2.3 of_flat_dt_match_machine

of_flat_dt_match_machine函数是匹配合适的machine_desc的关键:

/* 传入的第一个参数为NULL 传入的第二个参数为arch_get_next_mach arch_get_next_mach的原理非常简单:初始化一个静态局部变量为__arch_info_begin, 每次被调用时该变量(指针)+1并返回,如果超出了__arch_info_end,则返回NULL */ const void * __init of_flat_dt_match_machine(const void *default_match, const void * (*get_next_compat)(const char * const**)) { const void *data = NULL; const void *best_data = default_match; const char *const *compat; unsigned long dt_root; unsigned int best_score = ~1, score = 0; /* dt_root = 0 */ dt_root = of_get_flat_dt_root(); /* 遍历所有machine_desc,将machine_desc的dt_compat保存到compat compat指向一系列字符串(一个machine_desc也可能支持多个单板) */ while ((data = get_next_compat(&compat))) { /* DTB根节点的compatible属性值是一系列字符串,假设为"aaa", "bbb", "ccc" machine_desc的dt_compat(指针的指针)也指向一系列字符串,假设为"xxx", "ccc" 第一轮比较(score = 0): 1、score++, compatible的"aaa"dt_compat的"xxx" 2、score++, compatible的"bbb"dt_compat的"xxx" 3、score++, compatible的"ccc"dt_compat的"xxx" 第二轮比较(score = 0): 1、score++, compatible的"aaa"dt_compat的"ccc" 2、score++, compatible的"bbb"dt_compat的"ccc" 3、score++, compatible的"ccc"dt_compat的"ccc",此时匹配上,返回score(值为3) */ score = of_flat_dt_match(dt_root, compat); /* 记录得分最低(最匹配)的machine_desc */ if (score > 0 && score /* 打印根节点的compatible属性值 */ ...... return NULL; } /* 打印根节点的model属性值,若不存在则打印compatible属性值 */ pr_info("Machine model: %s\n", of_flat_dt_get_machine_name()); return best_data; }

至此,如何根据DTB的根节点的compatible属性匹配machine_desc就介绍完了。

3 内核对设备树中运行时配置信息的处理

kernel使用setup_arch ==> setup_machine_fdt ==> early_init_dt_scan_nodes来处理DTB中的运行时配置信息:

void __init early_init_dt_scan_nodes(void) { int rc = 0; /* 获取/chosen节点的信息 */ rc = of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line); if (!rc) pr_warn("No chosen node found, continuing without\n"); /* 获取根节点的{size,address}-cells属性值,之后才方便解析根节点的子节点的reg属性 */ of_scan_flat_dt(early_init_dt_scan_root, NULL); /* 解析/memory节点,设置内存信息 */ of_scan_flat_dt(early_init_dt_scan_memory, NULL); } 3.1 of_scan_flat_dt

要进一步了解其中的细节,我们需要先弄清楚of_scan_flat_dt函数做了什么:

/** * 遍历DTB的节点,直到参数传入的回调函数it返回非0值 */ int __init of_scan_flat_dt(int (*it)(unsigned long node, const char *uname, int depth, void *data), void *data) { /* blob指向DTB在内存中的起始地址 */ const void *blob = initial_boot_params; const char *pathp; int offset, rc = 0, depth = -1; /* 若设备树不存在则返回 */ if (!blob) return 0; /* 从根节点开始遍历 */ for (offset = fdt_next_node(blob, -1, &depth); /* 如果找到了有效的节点并且回调函数it返回0,则执行循环体 */ offset >= 0 && depth >= 0 && !rc; /* 继续遍历下一个节点 */ offset = fdt_next_node(blob, offset, &depth)) { /* 获取节点名 */ pathp = fdt_get_name(blob, offset, NULL); /* 对于老版本的设备树,得到的是节点的路径名,因此要去掉多余的前缀 */ /* 不过fdt_get_name已经考虑过这个问题了,这里有点多余 */ if (*pathp == '/') pathp = kbasename(pathp); /* 调用回调函数it offset: 节点起始位置在DTB的structure block中的偏移 pathp : 指向节点名 depth : 节点的深度(层次) data : 参数data,取决于调用者 */ rc = it(offset, pathp, depth, data); } return rc; }

不难看出,该函数只是一个遍历设备树节点的工具函数:遍历设备树节点,调用回调函数,如果回调函数判断该节点就是想要解析的节点,则进行相应的解析操作,并返回非0值,以指示该函数停止遍历动作。

3.2 解析/chosen节点

根据函数名字,以及3.1节的分析,猜也应该猜到传入的回调函数early_init_dt_scan_chosen用于解析/chosen节点:

/* offset: 节点起始位置在DTB的structure block中的偏移 pathp : 指向节点名 depth : 节点的深度(层次) data : boot_command_line,一个字符数组 */ int __init early_init_dt_scan_chosen(unsigned long node, const char *uname, int depth, void *data) { int l; const char *p; const void *rng_seed; pr_debug("search \"chosen\", depth: %d, uname: %s\n", depth, uname); /* 如果遍历到的不是作为根节点的子节点的chosen节点,则指示of_scan_flat_dt继续遍历下一个节点 */ if (depth != 1 || !data || (strcmp(uname, "chosen") != 0 && strcmp(uname, "chosen@0") != 0)) return 0; /* 当前节点是/chosen节点 */ /* 解析/chosen节点的initrd属性,设置全局变量phys_initrd_start和phys_initrd_size */ early_init_dt_check_for_initrd(node); /* 获取/chosen节点的bootargs属性的属性值 */ p = of_get_flat_dt_prop(node, "bootargs", &l); /* 如果属性存在,则p指向bootargs属性值——一个字符串,l记录了字符串的长度(含'\0') */ if (p != NULL && l > 0) /* 将启动参数拷贝到boot_command_line */ strlcpy(data, p, min(l, COMMAND_LINE_SIZE)); /* * CONFIG_CMDLINE配置项意味着如果u-boot传递的参数不含启动参数,那么 * CONFIG_CMDLINE就是默认的启动参数。如果含有启动参数,那么,是追加 * 还是覆盖已有的启动参数,取决于另外两个配置项CONFIG_CMDLINE_EXTEND * 和CONFIG_CMDLINE_FORCE。 */ #ifdef CONFIG_CMDLINE #if defined(CONFIG_CMDLINE_EXTEND) strlcat(data, " ", COMMAND_LINE_SIZE); strlcat(data, CONFIG_CMDLINE, COMMAND_LINE_SIZE); #elif defined(CONFIG_CMDLINE_FORCE) strlcpy(data, CONFIG_CMDLINE, COMMAND_LINE_SIZE); #else /* 如果DTB不带有启动参数,就使用kernel的启动参数——CONFIG_CMDLINE */ if (!((char *)data)[0]) strlcpy(data, CONFIG_CMDLINE, COMMAND_LINE_SIZE); #endif #endif /* CONFIG_CMDLINE */ pr_debug("Command line is: %s\n", (char*)data); /* 对rng-seed节点的解析,暂时不清楚这个东西 */ rng_seed = of_get_flat_dt_prop(node, "rng-seed", &l); if (rng_seed && l > 0) { ...... } /* 返回非0值,指示of_scan_flat_dt停止遍历 */ return 1; } 3.3 解析根节点的{size,address}-cells属性

在解析/memory节点之前,应该先得到根节点的{size,address}-cells属性值,因为/memory节点使用reg属性来存放内存的起始地址和长度,而解析reg属性少不了{size,address}-cells。

int __init early_init_dt_scan_root(unsigned long node, const char *uname, int depth, void *data) { const __be32 *prop; /* 验证当前节点是否是根节点 */ if (depth != 0) return 0; /* 设置默认值 */ dt_root_size_cells = OF_ROOT_NODE_SIZE_CELLS_DEFAULT; dt_root_addr_cells = OF_ROOT_NODE_ADDR_CELLS_DEFAULT; /* 如果有#size-cells属性则获取其值,并重新设置dt_root_size_cells */ prop = of_get_flat_dt_prop(node, "#size-cells", NULL); if (prop) /* 注意大小端的转换 */ dt_root_size_cells = be32_to_cpup(prop); pr_debug("dt_root_size_cells = %x\n", dt_root_size_cells); /* 如果存在#address-cells属性,则重新设置dt_root_addr_cells */ prop = of_get_flat_dt_prop(node, "#address-cells", NULL); if (prop) dt_root_addr_cells = be32_to_cpup(prop); pr_debug("dt_root_addr_cells = %x\n", dt_root_addr_cells); /* 停止遍历 */ return 1; } 3.4 解析/memory节点

是时候解析/memory节点了:

int __init early_init_dt_scan_memory(unsigned long node, const char *uname, int depth, void *data) { /* 获取/memory节点的device_type属性 */ const char *type = of_get_flat_dt_prop(node, "device_type", NULL); const __be32 *reg, *endp; int l; bool hotpluggable; /* /memory节点的device_type属性值必须是memory */ if (type == NULL || strcmp(type, "memory") != 0) return 0; /* 获取/memory节点的linux,usable-memory或reg属性值(存放了内存的起始地址和长度信息) */ reg = of_get_flat_dt_prop(node, "linux,usable-memory", &l); if (reg == NULL) reg = of_get_flat_dt_prop(node, "reg", &l); if (reg == NULL) return 0; endp = reg + (l / sizeof(__be32)); /* 获取hotpluggable属性值(指示是否可以热插拔) */ hotpluggable = of_get_flat_dt_prop(node, "hotpluggable", NULL); pr_debug("memory scan node %s, reg size %d,\n", uname, l); /* 遍历reg属性记录的一块或多块内存 */ while ((endp - reg) >= (dt_root_addr_cells + dt_root_size_cells)) { u64 base, size; /* 获取当前内存块的起始地址 */ base = dt_mem_next_cell(dt_root_addr_cells, ®); size = dt_mem_next_cell(dt_root_size_cells, ®); if (size == 0) continue; pr_debug(" - %llx , %llx\n", (unsigned long long)base, (unsigned long long)size); /* 对base和size进行一系列校验后,调用memblock_add添加内存块(struct memblock) */ early_init_dt_add_memory_arch(base, size); if (!hotpluggable) continue; /* 若当前内存块可以热插拔,那么标记之 */ if (early_init_dt_mark_hotplug_memory_arch(base, size)) pr_warn("failed to mark hotplug range 0x%llx - 0x%llx\n", base, base + size); } return 0; }

至此,内核对运行时配置信息的处理就介绍完了。

3.5 小结

在这里插入图片描述

4 内核对设备树中设备信息的处理 4.1 内核对DTB所在内存的处理

内核会保留DTB所占据的内存区域,因此DTB文件中的数据在kernel启动后也是可用的,相关源码的调用路径如下: 在这里插入图片描述

4.2 struct device_node和struct property

DTB文件的structure block区域记录了很多设备节点,每个节点又有很多属性,对此,kernel使用struct device_node来描述节点,使用struct property来描述属性。下面就介绍一下这两个结构的主要成员(不是全部成员):

struct device_node:

成员含义name指向节点的name属性的属性值(字符串),位于DTB的structure blockphandle节点的唯一的数字标识符full_name指向节点名字符串,该字符串紧跟着结构体本身properties指向节点的属性deadprops指向被移除的属性parent指向父节点child指向孩子节点sibling指向兄弟节点

struct property:

成员含义name指向属性名字符串,位于DTB的strings blocklength属性的长度valuevoid *类型,指向属性值,位于DTB的structure blocknext一个节点的所有属性构成一个链表 4.3 从DTB到struct device_node示例

kernel调用unflatten_device_tree函数,将DTB文件中的设备节点转换为一个个的struct device_node,这些结构体有着树状的层次,在分析相关源码之前,我们不妨先看一个设备树完成转换之后的结果,建立起总体上的认知。

首先给出一个设备树文件的示例,这个设备树文件并不完整,只是用作示例:

/ { model = "SMDK2416"; compatible = "samsung,s3c2416"; #address-cells = ; #size-cells = ; memory@30000000 { device_type = "memory"; reg = ; }; pinctrl@56000000 { name = "example_name"; compatible = "samsung,s3c2416-pinctrl"; }; };

该设备树文件被DTC编译为DTB之后,被u-boot传递给kernel,然后内核读取其节点信息,建立如下的由device_node构成的树状结构: 在这里插入图片描述 值得一提的是,为了突出device_node的name成员和full_name成员的差别,在上图中,我将没有name属性的节点的name成员置为,这源于:

static bool populate_node(const void *blob, int offset, void **mem, struct device_node *dad, struct device_node **pnp, bool dryrun) { ...... populate_properties(blob, offset, mem, np, pathp, dryrun); if (!dryrun) { np->name = of_get_property(np, "name", NULL); if (!np->name) /* 没有name属性,则name成员置为"" */ np->name = ""; } ...... }

但实际上,populate_properties函数会为没有name属性的节点创建name属性:

static void populate_properties(const void *blob, int offset, void **mem, struct device_node *np, const char *nodename, bool dryrun) { struct property *pp, **pprev = NULL; int cur; bool has_name = false; pprev = &np->properties; for (cur = fdt_first_property_offset(blob, offset); cur >= 0; cur = fdt_next_property_offset(blob, cur)) { ...... if (!strcmp(pname, "name")) has_name = true; ...... } /* With version 0x10 we may not have the name property, * recreate it here from the unit name if absent */ if (!has_name) { /* 为没有name属性的节点创建name属性 */ ...... } ...... } 4.4 unflatten_device_tree分析

有了上文的铺垫,我们就可以开始从源码的层面分析kernel如何根据DTB构建device_node树:

void __init unflatten_device_tree(void) { /* 根据DTB构建device_node树 */ __unflatten_device_tree(initial_boot_params, NULL, &of_root, early_init_dt_alloc_memory_arch, false); /* 设置of_aliases指向/aliases节点对应的device_node 设置of_chosen指向/chosen节点对应的device_node 对于of_chosen: 从其属性中找到属性名为stdout-path或linux,stdout-path的属性的属性值, 并根据该属性值获得标准输出设备对应的device_node,将其赋给of_stdout 对于of_aliases: 遍历其属性,跳过name、phandle、linux,phandle,对于其他的属性,如果 该属性的属性值指示了一个device_node,那么为这个device_node创建一个 struct alias_prop,并添加到aliases_lookup链表。 举一个例子来说,假设一个别名属性为i2c1 = "xxx",并且"xxx"指示了一个 device_node,那么为其创建的alias_prop的np成员指向相应的device_node; id成员为1,零长数组stem指向字符串"i2c"(数字部分作为id去掉了)。 */ of_alias_scan(early_init_dt_alloc_memory_arch); /* 看名字是用作测试的,具体不清楚 */ unittest_unflatten_overlay_base(); }

再看__unflatten_device_tree:

void *__unflatten_device_tree(const void *blob, struct device_node *dad, struct device_node **mynodes, void *(*dt_alloc)(u64 size, u64 align), bool detached) { int size; void *mem; /* 打印一些信息并校验DTB文件 */ ...... /* 第一次调用unflatten_dt_nodes,计算整个device_node树包括属性所需的全部内存 */ size = unflatten_dt_nodes(blob, NULL, dad, NULL); if (size int level; /* 遍历各个初始化函数指针所在的节,并调用初始化函数 */ for (level = 0; level of_platform_populate(node, NULL, NULL, NULL); of_node_put(node); } /* 上面是对特殊节点的处理,这里才是为大部分节点构建platform_device的函数 */ of_platform_default_populate(NULL, NULL, NULL); return 0; }

of_platform_default_populate只是of_platform_populate的简单包装,因此我们直接看后者:

/* root = NULL matches = of_default_bus_match_table lookup = NULL parent = NULL */ int of_platform_populate(struct device_node *root, const struct of_device_id *matches, const struct of_dev_auxdata *lookup, struct device *parent) { struct device_node *child; int rc = 0; /* 传入的root为NULL,因此这里执行of_find_node_by_path("/")获得根节点 */ root = root ? of_node_get(root) : of_find_node_by_path("/"); if (!root) return -EINVAL; pr_debug("%s()\n", __func__); pr_debug(" starting at: %pOF\n", root); /* 遍历根节点的每个孩子节点 */ for_each_child_of_node(root, child) { /* 为根节点的孩子节点创建platform_device(不是所有孩子节点,需要符合一定的条件) */ rc = of_platform_bus_create(child, matches, lookup, parent, true); if (rc) { of_node_put(child); break; } } /* 为根节点设置OF_POPULATED_BUS,标志着已经为其孩子节点创建完platform_device */ of_node_set_flag(root, OF_POPULATED_BUS); of_node_put(root); return rc; }

再看of_platform_bus_create:

/* bus : 该节点可能需要创建platform_device matches : 如果bus节点的compatible属性能和matches匹配上,说明其孩子节点也要创建platform_device(比如compatible的值为"simple-bus") lookup : 如果bus节点的compatible属性匹配lookup数组,那么相应的paltform_device的device.kobj.name设置为lookup数组中匹配元素的name(不是同一块内存) parent : 创建的paltform_device的device.parent = parent(device.kobj.parent = &device.parent.kobj) strict : bus节点是否一定要具备compatibile属性 */ static int of_platform_bus_create(struct device_node *bus, const struct of_device_id *matches, const struct of_dev_auxdata *lookup, struct device *parent, bool strict) { const struct of_dev_auxdata *auxdata; struct device_node *child; struct platform_device *dev; const char *bus_id = NULL; void *platform_data = NULL; int rc = 0; /* 如果要求必须有compatible属性,那么对于没有该属性的节点直接返回 */ if (strict && (!of_get_property(bus, "compatible", NULL))) { pr_debug("%s() - skipping %pOF, no compatible prop\n", __func__, bus); return 0; } /* 跳过由of_skipped_node_table指定的节点,这些节点不用创建platform_device */ if (unlikely(of_match_node(of_skipped_node_table, bus))) { pr_debug("%s() - skipping %pOF node\n", __func__, bus); return 0; } /* 已经为该节点及其孩子节点创建过platform_device,则返回 */ if (of_node_check_flag(bus, OF_POPULATED_BUS)) { pr_debug("%s() - skipping %pOF, already populated\n", __func__, bus); return 0; } /* 查找lookup数组中是否有匹配的数组项,如果有则取其name和platform_data用于后面的创建platform_device */ auxdata = of_dev_lookup(lookup, bus); if (auxdata) { bus_id = auxdata->name; platform_data = auxdata->platform_data; } /* 处理特殊的节点(兼容就版本的设备树) */ if (of_device_is_compatible(bus, "arm,primecell")) { /* * Don't return an error here to keep compatibility with older * device tree files. */ of_amba_device_create(bus, bus_id, platform_data, parent); return 0; } /* 为bus节点创建platform_device */ dev = of_platform_device_create_pdata(bus, bus_id, platform_data, parent); /* 如果bus节点的compatible属性比较特殊,比如是"simple-bus",则需要尝试为其子节点创建platform_device */ if (!dev || !of_match_node(matches, bus)) return 0; for_each_child_of_node(bus, child) { pr_debug(" create child: %pOF\n", child); /* 整个过程是递归进行的 */ rc = of_platform_bus_create(child, matches, lookup, &dev->dev, strict); if (rc) { of_node_put(child); break; } } of_node_set_flag(bus, OF_POPULATED_BUS); return rc; }

函数of_platform_device_create_pdata会调用of_device_alloc以及of_device_add创建并注册platform_device,具体的就不再跟踪了。

4.5.4 如何处理平台设备对应的设备节点的子节点

上文已经说过,对于compatible属性为simple-bus等特殊值的节点,kernel也会为其含有compatible属性的子节点创建platform_device。那么对于其他设备节点呢,怎么处理它们的子节点?所谓知子莫若父,它们的子节点应该交由父节点(也就是创建了platform_device的节点)来处理。仍以4.5.1节中i2c的例子来说明。

kernel为该节点创建platform_device后,将其注册到platform_bus_type,根据kernel的总线-设备-驱动模型,如果该节点的compatible属性samsung,s3c2410-i2c,匹配总线上的某个platform_driver,那么该驱动的probe函数会被调用,在该函数中,会为SoC的i2c控制器创建i2c_adapter,也会为连接在该控制器上的i2c接口的外设eeprom@50创建i2c_client。具体的函数调用过程放在4.6节,这里就不再多说了。

4.6 小结

在这里插入图片描述

5 内核中设备树相关头文件的总结

kernel源码树下的include/linux目录下存在着一些以of开头的头文件,这些头文件内是一些与设备树相关的函数的声明。下面将对这些头文件做一个分类。

5.1 声明处理DTB文件的函数的头文件 头文件内容of_fdt.h声明了dtb文件的相关操作函数,一般用不到,因为dtb文件在内核中被转换为device_node树,后者更易于使用 5.2 处理device_node的函数的头文件 头文件内容of.h提供设备树的一般处理函数,如 of_property_read_u32(读取某个属性的u32值)of_address.h地址相关的函数,如 of_get_address(获得reg属性中的addr、size值)of_dma.h处理设备树中DMA相关属性的函数of_gpio.hGPIO相关的函数of_graph.hGPU相关驱动中用到的函数,从设备树中获得GPU信息of_iommu.h暂不清楚of_irq.h中断相关的函数of_mdio.hMDIO (Ethernet PHY) APIof_net.hOF helpers for network devicesof_pci.hPCI相关函数of_pdt.h暂不清楚of_reserved_mem.h设备树中reserved_mem相关的函数 5.3 声明处理platform_device的函数的头文件 头文件内容of_platform.h声明了把device_node转换为platform_device时用到的函数of_device.h主要声明了struct device相关的函数,如 of_match_device 6 设备树在文件系统中的表示 6.1 /sys与设备树

所有设备树的信息存放于/sys/firmware目录下:

目录/文件含义/sys/firmware/fdt该文件表示原始DTB文件,可用hexdump -C /sys/firmware/fdt查看/sys/firmware/devicetree以目录结构呈现设备树,每个device_node对应一个目录,每个属性对应节点目录下的一个文件吗,比如根节点对应base目录,该目录下有compatible等文件

所有的platform_device会在/sys/devices/platform下对应一个目录,这些platform_device有来自设备树的,也有来自.c文件中手工注册的。由kernel根据设备树创建的platform_device对应的目录下存在一个名为of_node的软链接,链接向该platform_device对应的device_node对应的目录。

6.2 /proc与设备树

/proc/device-tree作为链接文件指向/sys/firmware/devicetree/base。

参考文献

[1] linux-kernel-5.4.26源码及文档 [2] 韦东山老师的设备树视频教程



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3